/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tools.lint.client.api;

import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** Registry which provides a list of checks to be performed on an Android project */
public abstract class IssueRegistry {
    private static List<Category> sCategories;
    private static Map<String, Issue> sIdToIssue;

    /**
     * Issue reported by lint (not a specific detector) when it cannot even
     * parse an XML file prior to analysis
     */
    public static final Issue PARSER_ERROR = Issue.create(
            "XmlParserError", //$NON-NLS-1$
            "Finds XML files that contain fatal parser errors",
            "XML files must be parsable.",
            Category.CORRECTNESS,
            10,
            Severity.ERROR,
            null,
            Scope.RESOURCE_FILE_SCOPE);

    /**
     * Returns the list of issues that can be found by all known detectors.
     *
     * @return the list of issues to be checked (including those that may be
     *         disabled!)
     */
    public abstract List<Issue> getIssues();

    /**
     * Creates a list of detectors applicable to the given cope, and with the
     * given configuration.
     *
     * @param client the client to report errors to
     * @param configuration the configuration to look up which issues are
     *            enabled etc from
     * @param scope the scope for the analysis, to filter out detectors that
     *            require wider analysis than is currently being performed
     * @param scopeToDetectors an optional map which (if not null) will be
     *            filled by this method to contain mappings from each scope to
     *            the applicable detectors for that scope
     * @return a list of new detector instances
     */
    final List<? extends Detector> createDetectors(
            LintClient client,
            Configuration configuration,
            EnumSet<Scope> scope,
            Map<Scope, List<Detector>> scopeToDetectors) {
        List<Issue> issues = getIssues();
        Set<Class<? extends Detector>> detectorClasses = new HashSet<Class<? extends Detector>>();
        Map<Class<? extends Detector>, EnumSet<Scope>> detectorToScope =
                new HashMap<Class<? extends Detector>, EnumSet<Scope>>();
        for (Issue issue : issues) {
            Class<? extends Detector> detectorClass = issue.getDetectorClass();
            if (detectorClasses.contains(detectorClass)) {
                continue;
            }

            // Determine if the issue is enabled
            if (!configuration.isEnabled(issue)) {
                continue;
            }

            // Determine if the scope matches
            if (!scope.containsAll(issue.getScope())) {
                continue;
            }

            detectorClass = client.replaceDetector(detectorClass);

            if (scopeToDetectors != null) {
                EnumSet<Scope> s = detectorToScope.get(detectorClass);
                if (s == null) {
                    detectorToScope.put(detectorClass, issue.getScope());
                } else {
                    EnumSet<Scope> union = EnumSet.copyOf(s);
                    union.addAll(issue.getScope());
                    detectorToScope.put(detectorClass, union);
                }
            }

            assert detectorClass != null : issue.getId();
            detectorClasses.add(detectorClass);
        }

        List<Detector> detectors = new ArrayList<Detector>(detectorClasses.size());
        for (Class<? extends Detector> clz : detectorClasses) {
            try {
                Detector detector = clz.newInstance();
                detectors.add(detector);

                if (scopeToDetectors != null) {
                    EnumSet<Scope> union = detectorToScope.get(clz);
                    for (Scope s : union) {
                        List<Detector> list = scopeToDetectors.get(s);
                        if (list == null) {
                            list = new ArrayList<Detector>();
                            scopeToDetectors.put(s, list);
                        }
                        list.add(detector);
                    }

                }
            } catch (Throwable t) {
                client.log(t, "Can't initialize detector %1$s", clz.getName()); //$NON-NLS-1$
            }
        }

        return detectors;
    }

    /**
     * Returns true if the given id represents a valid issue id
     *
     * @param id the id to be checked
     * @return true if the given id is valid
     */
    public final boolean isIssueId(String id) {
        return getIssue(id) != null;
    }

    /**
     * Returns true if the given category is a valid category
     *
     * @param name the category name to be checked
     * @return true if the given string is a valid category
     */
    public final boolean isCategoryName(String name) {
        for (Category c : getCategories()) {
            if (c.getName().equals(name) || c.getFullName().equals(name)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the available categories
     *
     * @return an iterator for all the categories, never null
     */
    public List<Category> getCategories() {
        if (sCategories == null) {
            final Set<Category> categories = new HashSet<Category>();
            for (Issue issue : getIssues()) {
                categories.add(issue.getCategory());
            }
            List<Category> sorted = new ArrayList<Category>(categories);
            Collections.sort(sorted);
            sCategories = Collections.unmodifiableList(sorted);
        }

        return sCategories;
    }

    /**
     * Returns the issue for the given id, or null if it's not a valid id
     *
     * @param id the id to be checked
     * @return the corresponding issue, or null
     */
    public final Issue getIssue(String id) {
        if (sIdToIssue == null) {
            List<Issue> issues = getIssues();
            sIdToIssue = new HashMap<String, Issue>(issues.size());
            for (Issue issue : issues) {
                sIdToIssue.put(issue.getId(), issue);
            }
        }
        return sIdToIssue.get(id);
    }
}
